package com.core.gwt.ui.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.IsEditor;
import com.google.gwt.editor.client.LeafValueEditor;
import com.google.gwt.editor.client.adapters.TakesValueEditor;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.text.shared.Renderer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.HasConstrainedValue;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SimpleKeyProvider;

/**
 * An implementation of {@link HasConstrainedValue} based on a group of
 * {@link RadioButton} widgets.<br>
 * <br>
 * 
 * A {@link Renderer} is used to get user-presentable strings to be used as the
 * radio button labels.<br>
 * <br>
 * If {@link HasConstrainedValue#setValue(Object)} is called with a value that
 * is not present in the acceptable value set, all the radio buttons get
 * de-selected and no exception is thrown.
 * 

 * 
 * @param <T>
 *            the value type
 */
public class OptionBox<T> extends Composite implements HasConstrainedValue<T>,
		IsEditor<LeafValueEditor<T>> {

	public interface Resources extends ClientBundle {
		@Source("OptionBox.css")
		Style optionBoxStyle();
	}

	public interface Style extends CssResource {
		String optionBoxSection();
	}

	private static Resources DEFAULT_RESOURCES;

	private static Resources getDefaultResources() {
		if (DEFAULT_RESOURCES == null) {
			DEFAULT_RESOURCES = GWT.create(Resources.class);
		}
		return DEFAULT_RESOURCES;
	}

	private Resources resources;
	private Style style;
	private final FlowPanel panel;
	private final Renderer<T> renderer;
	private final ProvidesKey<T> keyProvider;
	private String groupName;
	private final List<T> values = new ArrayList<T>();
	private final List<HandlerRegistration> handlerRegistrations = new ArrayList<HandlerRegistration>();
	private final List<SimplePanel> buttonPanels = new ArrayList<SimplePanel>();
	private final List<RadioButton> buttons = new ArrayList<RadioButton>();
	private final Map<RadioButton, Integer> buttonIndexMap = new HashMap<RadioButton, Integer>();
	private final Map<Object, Integer> keyIndexMap = new HashMap<Object, Integer>();
	private T value;
	private LeafValueEditor<T> editor;

	public OptionBox(final Renderer<T> renderer) {
		this(renderer, new SimpleKeyProvider<T>());
	}

	public OptionBox(final Renderer<T> renderer,
			final ProvidesKey<T> keyProvider) {

		resources = getDefaultResources();
		style = resources.optionBoxStyle();
		style.ensureInjected();

		panel = new FlowPanel();
		initWidget(panel);

		this.renderer = renderer;
		this.keyProvider = keyProvider;

		groupName = HTMLPanel.createUniqueId();

		rebuild();
	}

	@Override
	public HandlerRegistration addValueChangeHandler(
			final ValueChangeHandler<T> handler) {
		return addHandler(handler, ValueChangeEvent.getType());
	}

	@Override
	public LeafValueEditor<T> asEditor() {
		if (editor == null) {
			editor = TakesValueEditor.of(this);
		}
		return editor;
	}

	@Override
	public T getValue() {
		return value;
	}

	@Override
	public void setAcceptableValues(final Collection<T> values) {
		this.values.clear();
		this.values.addAll(values);
		rebuild();
	}

	@Override
	public void setValue(final T value) {
		setValue(value, false);
	}

	@Override
	public void setValue(final T value, final boolean fireEvents) {
		final T oldValue = this.value;
		this.value = value;
		update();
		if (fireEvents) {
			ValueChangeEvent.fireIfNotEqual(this, oldValue, value);
		}
	}

	private void addHandlers() {
		for (final RadioButton button : buttons) {
			button.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(final ClickEvent event) {
					final T newValue = values.get(buttonIndexMap.get(button));
					setValue(newValue, true);
				}
			});
		}
	}

	private void rebuild() {
		removeHandlers();

		for (final SimplePanel buttonPanel : buttonPanels) {
			panel.remove(buttonPanel);
		}
		buttonPanels.clear();
		buttons.clear();
		buttonIndexMap.clear();
		keyIndexMap.clear();

		for (int i = 0; i < values.size(); i++) {
			final T value = values.get(i);
			keyIndexMap.put(keyProvider.getKey(value), i);

			final RadioButton button = new RadioButton(groupName,
					renderer.render(value));
			buttons.add(button);
			buttonIndexMap.put(button, i);

			final SimplePanel buttonPanel = new SimplePanel();
			if (i > 0) {
				buttonPanel.addStyleName(style.optionBoxSection());
			}
			buttonPanel.setWidget(button);
			panel.add(buttonPanel);
			buttonPanels.add(buttonPanel);
		}
		addHandlers();

		update();
	}

	private void removeHandlers() {
		for (final HandlerRegistration handlerRegistration : handlerRegistrations) {
			if (handlerRegistration != null) {
				handlerRegistration.removeHandler();
			}
		}
		handlerRegistrations.clear();
	}

	private void update() {
		for (final RadioButton button : buttons) {
			button.setValue(false);
		}
		final Object key = keyProvider.getKey(value);
		if (keyIndexMap.containsKey(key)) {
			buttons.get(keyIndexMap.get(key)).setValue(true);
		}
	}
}
